﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace Bettzueche.RLLibrary.Types
{
    /// <summary>
    /// Beschreibt eine Dimension mit kontinuierlichem Wertebereich und ihre Partitionierung.
    /// </summary>
    /// <remarks>
    /// <para>Sei bspw. ein Unebenheitsmaß gegeben, dass zwischen 0 und 1 liegt. Die (unendlichen vielen) Werte im Interval [0.0 ; 1.0]
    /// sollen in vier Bereiche aufgeteilt werden. So erzeugt man sich folgende Dimension:</para>
    /// <para><code>
    /// Dimension u = new Dimension(0.0, 1.0, 4) { Name = "Unebenheit" };
    /// </code></para>
    /// <para>Die Unebenheiten werden in folgende Teilintervalle untergliedert:
    /// [0.0 ; 0.25) [0.25 ; 0.5) [0.5 ; 0.75) [0.75 ; 1.0)
    /// Das erste Intervall hat den Index 0, das letzte den index 3.</para>
    /// <para>
    /// <code>
    /// double x = 0.3;
    /// int intervallNum = u.GetPartitionNumber(x); // liefert 1
    /// intervallNum = u.GetPartitionNumber(0.0); // liefert 0
    /// intervallNum = u.GetPartitionNumber(-0.5); // liefert Dimension.OutOfBounds
    /// intervallNum = u.GetPartitionNumber(1.0); // liefert Dimension.OutOfBounds
    /// </code>
    /// </para>
    /// </remarks>
    [DebuggerDisplay("Tiling = [{Min},{Max}) / {Partitions}")]
    public class Tiling : IEquatable<Tiling>
    {
        #region Fields

        /// <summary>
        /// Indicates, that a value is less than Min or greater than Max, so it belongs to the '<c>OutOfBounds</c> partition'.
        /// </summary>
        public static readonly int OutOfBounds = Int32.MaxValue;
        /// <summary>
        /// The intervall length Max - Min (calculated autom. when set Min, Max or Partitions)
        /// </summary>
        protected double _intervallLength;
        /// <summary>
        /// The length of one partition (calculated autom. when set Min, Max or Partitions)
        /// </summary>
        protected double _partitionLength;
        protected double _sigma;

        #endregion


        #region Properties

        /// <summary>
        /// Gets or sets the name (optional).
        /// </summary>
        /// <value>
        /// The name.
        /// </value>
        public String Name { get; set; }

        /// <summary>
        /// Gets or sets the minimum value (inclusive), that is in a partition.<para>
        /// The partition number of values less than <c>Min</c> is <see cref="Tiling#OutOfBounds"/></para>
        /// </summary>
        /// <value>
        /// The minimum value (inclusive). Should be less than or equal to <c>Max</c>.
        /// </value>
        public double Min {
            get { return _min; }
            set {
                _min = value;
                UpdateParameters();
            }
        }
        double _min;

        /// <summary>
        /// Gets or sets the maximum (exclusive), that is in a partition.<para>
        /// The partition number of values greater than <c>Max</c> is <see cref="Tiling#OutOfBounds"/></para>.
        /// </summary>
        /// <value>
        /// The maximum value (exclusive). Should be greater than or equal to <c>Min</c>.
        /// </value>
        public double Max {
            get { return _max; }
            set {
                _max = value;
                UpdateParameters();
            }
        }
        double _max;



        /// <summary>
        /// Gets or sets the number of partitions, intervals respectively.<para>
        /// (Without the <c>OutOfBoundPartition</c>)</para>
        /// </summary>
        /// <value>
        /// The number of partitions. Should be greater than 0.
        /// </value>
        public int Partitions {
            get { return _partitions; }
            set {
                _partitions = value;
                UpdateParameters();
            }
        }
        int _partitions;

        #endregion


        #region Constructor(s)

        protected Tiling() { }

        /// <summary>
        /// Initializes a new instance of the <see cref="Tiling"/> class.
        /// </summary>
        /// <param name="min">The minimum.</param>
        /// <param name="max">The maximum.</param>
        public Tiling(double min, double max) : this(min, max, 1) {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Tiling"/> class.
        /// </summary>
        /// <param name="min">The minimum.</param>
        /// <param name="max">The maximum.</param>
        /// <param name="partitions">The partitions.</param>
        public Tiling(double min, double max, int partitions) {
            Partitions = partitions;
            Min = min;
            Max = max;
        }

        #endregion


        #region Methods

        /// <summary>
        /// Gets the partition number the specified value lies in.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>The partition number the specified value lies in. <see cref="Tiling#OutOfBounds"/> if value 
        /// is less than Min, greater or equal Max, respectively</returns>
        public virtual int GetPartitionNumber(double value) {
            int idx = Tiling.OutOfBounds;
            // Sonderfaelle
            if (Partitions <= 0) {
                return idx;
            }
            if (value < Min) {
                return idx;
            }
            if (value >= Max) {
                return idx;
            }
            // Wert verschieben und Index berechnen
            value -= Min; // rechts versch., wenn Min pos.; links sonst
            idx = (int)(value / _partitionLength);
            return idx;
        }

        //
        // Future planning: Nichtbinäre Merkmale:
        //

        double GetFuzzyValue(double value, int partition) {
            double median = GetMedian(partition);
            double distanceFromMedian = Math.Abs(median - value);
            double fuzzyVal = (2 * distanceFromMedian) / _partitionLength;
            return fuzzyVal;
        }

        double GetFuzzyGaussValue(double value, int partition) {
            // TODO GetFuzzyGaussValue testen, dokumentieren
            double retVal = Math.Abs(value - GetMedian(partition));
            retVal = Math.Pow(retVal, 2.0);
            retVal /= 2*Math.Pow(_sigma, 2.0);
            retVal = Math.Exp(-retVal);
            return retVal;
            //return Math.Exp(-Math.Pow(Math.Abs(value - GetMedian(partition)),2.0) / (2*Math.Pow(_sigma,2.0)));
        }

        //-------------------------------------------------------------

        /// <summary>
        /// Gets the Feature.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public virtual Feature GetFeature(double value) {
            Feature f = new Feature(-1, true, 0.0);
            f.ID = GetPartitionNumber(value);
            f.FuzzyValue = GetFuzzyValue(value, f.ID);
            return f;
        }

        /// <summary>
        /// Gets the median of the specified partition number, that is the midpoint of the sub-interval.
        /// </summary>
        /// <param name="partition">The partition index.</param>
        /// <returns>
        /// The median (midpoint) value of the specified partition.<para>
        /// NegativInfinity, if partition is less than 0. PositivInfinity, if partition is greater or equal <see cref="Tiling#Partitions"/>.</para>
        /// </returns>
        public virtual double GetMedian(int partition) {
            if (partition < 0) {
                return Double.NegativeInfinity;;
            }
            if (partition >= Partitions) {
                return Double.PositiveInfinity;
            }
            double median = _partitionLength * (partition + 0.5) + Min;
            return median;
        }

        private void UpdateParameters() {
            _intervallLength = _max - _min;
            _partitionLength = _partitions == 0.0 ? 0.0 : _intervallLength / _partitions;
            _sigma = _partitionLength / 6.0; // Normalvertielung: 99.7% liegen in Median +/- 3*Sigma
        }

        #endregion

        #region Overrides

        /// <summary>
        /// Returns a <see cref="System.String" /> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String" /> that represents this instance.
        /// </returns>
        public override string ToString() {
            StringBuilder sb = new StringBuilder("Dimension {");
            sb.Append(Min);
            sb.Append(", [");
            sb.Append(Min).Append(", ").Append(Max).Append(")}");
            return sb.ToString();
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode() {
            int hash = 7;
            hash = 17 * hash + Partitions;
            hash = hash ^ Min.GetHashCode();
            hash = hash ^ Max.GetHashCode();
            return hash;
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public override bool Equals(object obj) {
            Tiling other = obj as Tiling;
            return other == null ? false : Equals(other);
        }

        /// <summary>
        /// Determines whether the specified <c>Dimension</c>, is equal to this instance.
        /// </summary>
        /// <param name="dim">The <see cref="Bettzueche.RLLibrary.Types.Tiling">Dimension</see> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <c>Dimension</c> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public bool Equals(Tiling dim) {
            if (dim == null)
                return false;
            if (dim.Partitions != this.Partitions)
                return false;
            if (dim.Min != this.Min)
                return false;
            if (dim.Max != this.Max)
                return false;
            return true;
        }

        #endregion
    }
}
